package com.aliothservice.catfish.identity;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.multipart.MultipartFile;

import com.aliothservice.catfish.SessionManager;
import com.aliothservice.catfish.identity.data.EnterprisePending;
import com.aliothservice.catfish.identity.data.IdentityDataManager;
import com.aliothservice.catfish.identity.data.IndividualPending;
import com.aliothservice.catfish.identity.data.User;
import com.aliothservice.catfish.identity.data.WithdrawRequest;
import com.aliothservice.catfish.identity.request.AccountBalanceRequest;
import com.aliothservice.catfish.identity.request.CompleteEnterpriseInfoRequest;
import com.aliothservice.catfish.identity.request.CompleteIndividualInfoRequest;
import com.aliothservice.catfish.identity.request.GetCertificateRequest;
import com.aliothservice.catfish.identity.request.GetVerificationCodeRequest;
import com.aliothservice.catfish.identity.request.PendingEnterpriseInfoRequest;
import com.aliothservice.catfish.identity.request.PendingIndividualInfoRequest;
import com.aliothservice.catfish.identity.request.ResetPasswordRequest;
import com.aliothservice.catfish.identity.request.SetUserDataRequest;
import com.aliothservice.catfish.identity.request.SignInRequest;
import com.aliothservice.catfish.identity.request.SignOutRequest;
import com.aliothservice.catfish.identity.request.SignUpRequest;
import com.aliothservice.catfish.identity.request.UpdateEnterpriseCertInfoRequest;
import com.aliothservice.catfish.identity.request.UpdateEnterpriseInfoRequest;
import com.aliothservice.catfish.identity.request.UpdateIndividualCertInfoRequest;
import com.aliothservice.catfish.identity.request.UpdateIndividualInfoRequest;
import com.aliothservice.catfish.identity.request.UpdatePasswordRequest;
import com.aliothservice.catfish.identity.request.UserDataRequest;
import com.aliothservice.catfish.identity.request.UserInfoRequest;
import com.aliothservice.catfish.identity.request.ValidateVerificationCodeRequest;
import com.aliothservice.catfish.identity.request.WithdrawRequestRequest;
import com.aliothservice.catfish.message.MessageController;
import com.aliothservice.catfish.message.data.Message;
import com.mongodb.gridfs.GridFSDBFile;
import com.taobao.api.ApiException;
import com.taobao.api.DefaultTaobaoClient;
import com.taobao.api.TaobaoClient;
import com.taobao.api.request.AlibabaAliqinFcSmsNumSendRequest;
import com.taobao.api.response.AlibabaAliqinFcSmsNumSendResponse;

public class IdentityController {
	
	public Map<String, String> getVerificationCode(GetVerificationCodeRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// frequency check
		ValueOperations<String, Object> valueOp = redisTemplate.opsForValue();
		String frequencyMarker = (String)valueOp.get(req.getPhoneNumber() + IdentityController.FREQUENCY_KEY_SUFFIX);
		if (null != frequencyMarker) {
			if (logger.isDebugEnabled()) {
				logger.debug("getVerificationCode - " + req.getPhoneNumber() + " requests verification code too often, rejected.");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "request too often");
			return map;
		}
		valueOp.set(req.getPhoneNumber()+IdentityController.FREQUENCY_KEY_SUFFIX, req.getPhoneNumber(), 60L, TimeUnit.SECONDS);
		
		String verString = (String)valueOp.get(req.getPhoneNumber() + IdentityController.VERIFICATION_KEY_SUFFIX);
		if (null == verString) {
			// generate verification code
//			String code = "1111";
			Random random = new Random();
			String code = String.format("%d%d%d%d", Integer.valueOf(random.nextInt(10)), Integer.valueOf(random.nextInt(10)), Integer.valueOf(random.nextInt(10)), Integer.valueOf(random.nextInt(10)));
			TaobaoClient client = new DefaultTaobaoClient("http://gw.api.taobao.com/router/rest", "23429673", "ca5614e9fe27c5da97219a53f675233f");
			AlibabaAliqinFcSmsNumSendRequest aliReq = new AlibabaAliqinFcSmsNumSendRequest();
			aliReq.setExtend("extend");
			aliReq.setSmsType("normal");
			aliReq.setSmsFreeSignName("中冷云达");
			aliReq.setSmsParamString("{code:'"+code+"'}");
			aliReq.setRecNum(req.getPhoneNumber());
			aliReq.setSmsTemplateCode("SMS_13038069");
			try {
				AlibabaAliqinFcSmsNumSendResponse rsp = client.execute(aliReq);
				if (!rsp.isSuccess()) {
					logger.error("getVerificationCode - failed to send sms to " + req.getPhoneNumber());
				}
			} catch (ApiException e) {
				logger.error("getVerificationCode - failed to send sms to " + req.getPhoneNumber());
			}
			valueOp.set(req.getPhoneNumber()+IdentityController.VERIFICATION_KEY_SUFFIX, code, 30L, TimeUnit.MINUTES);
			if (logger.isInfoEnabled()) {
				logger.info("getVerificationCode - code has been generated for " + req.getPhoneNumber());
			}
		} else {
			if (logger.isInfoEnabled()) {
				logger.info("getVerificationCode - reuse existing code for " + req.getPhoneNumber());
			}
			TaobaoClient client = new DefaultTaobaoClient("http://gw.api.taobao.com/router/rest", "23429673", "ca5614e9fe27c5da97219a53f675233f");
			AlibabaAliqinFcSmsNumSendRequest aliReq = new AlibabaAliqinFcSmsNumSendRequest();
			aliReq.setSmsType("normal");
			aliReq.setSmsFreeSignName("中国冷链");
			aliReq.setSmsParamString("{\"code\":\""+verString+"\"}");
			aliReq.setRecNum(req.getPhoneNumber());
			aliReq.setSmsTemplateCode("SMS_13046326");
			try {
				AlibabaAliqinFcSmsNumSendResponse rsp = client.execute(aliReq);
				if (!rsp.isSuccess()) {
					logger.error("getVerificationCode - failed to send sms to " + req.getPhoneNumber());
				}
			} catch (ApiException e) {
				logger.error("getVerificationCode - failed to send sms to " + req.getPhoneNumber());
			}
		}
		
		// TODO send out verification code
		
		return map;
	}
	
	public Map<String, String> signUp(SignUpRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// check verification code
		ValueOperations<String, Object> valueOp = redisTemplate.opsForValue();
		String verString = (String)valueOp.get(req.getPhoneNumber() + IdentityController.VERIFICATION_KEY_SUFFIX);
		if ((null==verString) || (0!=verString.compareTo(req.getVerificationCode()))) {
			if (logger.isDebugEnabled()) {
				logger.debug("signUp - verification code is invalid or expired");
			}
			map.put("errorCode", "300");
			map.put("errorMessage", "code expired or invalid code");
			return map;
		}
		
		// locate user
		User existingUser = identityDataManager.findUser(req.getPhoneNumber());
		if (null != existingUser) {
			if (logger.isInfoEnabled()) {
				logger.info("signUp - sign up with existing account " + req.getPhoneNumber());
			}
			map.put("errorCode", "400");
			map.put("errorMessaged", "signed up already");
			return map;
		}
		
		// create user
		identityDataManager.createUser(req.getPhoneNumber(), req.getPassword());
		if (logger.isInfoEnabled()) {
			logger.info("signUp - new user created " + req.getPhoneNumber());
		}
		
		// get token
		String token = sessionManager.createRestToken(req.getPhoneNumber());
		map.put("token", token);
		
		// post welcome message
		Message welcomeMessage = new Message();
		welcomeMessage.setId(UUID.randomUUID().toString());
		welcomeMessage.setDate(new Date());
		welcomeMessage.setTitle("欢迎来到中国冷链");
		welcomeMessage.setContent("中国冷链在线平台是包含了资源发布方和资源需求方的平台，您可以寻找车源，库源，货源并在线下订单，也可以认证为资源发布方，发布车源，库源和货源。一切交易在平台监管和担保下完成。");
		welcomeMessage.setPayload("");
		welcomeMessage.setFlag(Message.FLAG_BLUE);
		welcomeMessage.setSourceModule("Identity");
		welcomeMessage.setSourceEvent("signUp");
		messageController.postMessage(welcomeMessage, req.getPhoneNumber());
		
		return map;
	}
	
	public Map<String, String> validateVerificationCode(ValidateVerificationCodeRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// check verification code
		ValueOperations<String, Object> valueOp = redisTemplate.opsForValue();
		String verString = (String)valueOp.get(req.getPhoneNumber() + IdentityController.VERIFICATION_KEY_SUFFIX);
		if ((null==verString) || (0!=verString.compareTo(req.getVerificationCode()))) {
			if (logger.isDebugEnabled()) {
				logger.debug("validateVerificationCode - verification code is invalid or expired");
			}
			map.put("errorCode", "300");
			map.put("errorMessage", "code expired or invalid code");
			return map;
		}
		
		return map;
	}
	
	// allows signing in from multiple end points
	public Map<String, String> signIn(SignInRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// locate user
		User user = identityDataManager.findUser(req.getPhoneNumber());
		if (null == user) {
			if (logger.isDebugEnabled()) {
				logger.info("signIn - unrecognized user " + req.getPhoneNumber());
			}
			map.put("errorCode", "300");
			map.put("errorMessage", "user not found");
			return map;
		}
		
		// check password
		if (0 != user.getPassword().compareTo(req.getPassword())) {
			if (logger.isDebugEnabled()) {
				logger.info("signIn - incorrect password " + req.getPhoneNumber());
			}
			map.put("errorCode", "301");
			map.put("errorMessage", "validation failed");
			return map;
		}
		
		// get token
		String token = sessionManager.createRestToken(req.getPhoneNumber());
		map.put("token", token);
		
		// update notification parameters as necessary
		if ((null!=req.getMobileDeviceOS()) || (null!=req.getDeviceToken())) {
			Query query = new Query();
			Criteria criteria = Criteria.where("phoneNumber").is(req.getPhoneNumber());
			query.addCriteria(criteria);
			Update update = new Update();
			if (null != req.getMobileDeviceOS()) {
				update.set("mobileDeviceOS", req.getMobileDeviceOS());
			}
			if (null != req.getDeviceToken()) {
				update.set("deviceToken", req.getDeviceToken());
			}
			if (!mongoTemplate.updateFirst(query, update, User.class).isUpdateOfExisting()) {
				logger.error("cannot update user's notification parameters");
			}
		}
		
		return map;
	}
	
	public Map<String, String> signOut(SignOutRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		String key = (String)sessionManager.valueForRestToken(req.getToken());
		if (null != key) {
			sessionManager.clearRestToken(key);
		}
		
		return map;
	}
	
	public Map<String, String> resetPassword(ResetPasswordRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// check verification code
		ValueOperations<String, Object> valueOp = redisTemplate.opsForValue();
		String verString = (String)valueOp.get(req.getPhoneNumber() + IdentityController.VERIFICATION_KEY_SUFFIX);
		if ((null==verString) || (0!=verString.compareTo(req.getVerificationCode()))) {
			if (logger.isDebugEnabled()) {
				logger.debug("signUp - verification code is invalid or expired");
			}
			map.put("errorCode", "300");
			map.put("errorMessage", "code expired or invalid code");
			return map;
		}
		
		// locate user
		User user = identityDataManager.findUser(req.getPhoneNumber());
		if (null == user) {
			if (logger.isDebugEnabled()) {
				logger.info("resetPassword - unrecognized user " + req.getPhoneNumber());
			}
			map.put("errorCode", "301");
			map.put("errorMessage", "user not found");
			return map;
		}
		
		// reset password
		if (!identityDataManager.updatePassword(user, req.getPassword())) {
			if (logger.isErrorEnabled()) {
				logger.error("resetPassword - failed to update password");
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update password");
		}
		
		return map;
	}
	
	public Map<String, String> updatePassword(UpdatePasswordRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate new password
		String newPassword = req.getNewPassword();
		if (null == newPassword) {
			if (logger.isDebugEnabled()) {
				logger.debug("updatePassword - new password is missing");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "missing password");
			return map;
		}
		if ((6>newPassword.length()) || (18<newPassword.length())) {
			if (logger.isDebugEnabled()) {
				logger.debug("updatePassword - new password is too short or too long");
			}
			map.put("errorCode", "401");
			map.put("errorMessage", "invalid password");
			return map;
		}
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// locate user
		User user = identityDataManager.findUser(phoneNumber);
		if (null == user) {
			if (logger.isDebugEnabled()) {
				logger.info("updatePassword - unrecognized user " + phoneNumber);
			}
			map.put("errorCode", "402");
			map.put("errorMessage", "user not found");
			return map;
		}
		
		// validate original password
		if (0 != user.getPassword().compareTo(req.getPassword())) {
			if (logger.isDebugEnabled()) {
				logger.debug("updatePassword - incorrect original password");
			}
			map.put("errorCode", "401");
			map.put("errorMessage", "invalid password");
			return map;
		}
		
		// update password
		if (!identityDataManager.updatePassword(user, req.getNewPassword())) {
			if (logger.isErrorEnabled()) {
				logger.error("updatePassword - failed to update password");
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update password");
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, Object> userInfo(UserInfoRequest req) {
		Map<String, Object> map = new HashMap<String, Object>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// locate user
		User user = identityDataManager.findUser(phoneNumber);
		if (null == user) {
			if (logger.isDebugEnabled()) {
				logger.info("userInfo - unrecognized user " + phoneNumber);
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to locate user");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		// compose response
		map.put("accountStatus", user.getAccountStatus());
		if (null != user.getIndividualInfo()) {
			Map<String, String> individualMap = user.getIndividualInfo().toMap();
			map.put("individualInfo", individualMap);
		}
		if (null != user.getEnterpriseInfo()) {
			Map<String, String> enterpriseMap = user.getEnterpriseInfo().toMap();
			map.put("enterpriseInfo", enterpriseMap);
		}
		if (null != user.getRejectedIndividualInfo()) {
			map.put("rejectedIndividualInfo", user.getRejectedIndividualInfo().toMap());
		}
		if (null != user.getRejectedEnterpriseInfo()) {
			map.put("rejectedEnterpriseInfo", user.getRejectedEnterpriseInfo().toMap());
		}
		if (null != user.getAvatarId()) {
			map.put("avatarId", user.getAvatarId());
		}
		
		return map;
	}
	
	public Map<String, String> setUserData(SetUserDataRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate parameters
		if ((null==req.getKey()) || (null==req.getValue())) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// perform set operation
		if (!identityDataManager.setUserData(phoneNumber, req)) {
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to set");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;		
	}
	
	public Map<String, String> userData(UserDataRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate parameters
		if (null == req.getKey()) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// perform retrieval
		String value = identityDataManager.userData(phoneNumber, req);
		if (null != value) {
			map.put("value", value);
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;		
	}
	
	public Map<String, String> withdrawRequest(WithdrawRequestRequest req) throws UnsupportedEncodingException {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate parameters
		if ((null==req.getAlipayAccount()) || (null==req.getAlipayUserName()) || (null==req.getWithdrawAmount())) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// create withdraw request
		if (!identityDataManager.withdrawRequest(phoneNumber, req)) {
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to process");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;		
	}
	
	public void withdrawRequestCompleted(String batchNumber) {
		// locate withDrawRequest
		Query query = new Query();
		Criteria userCriteria = Criteria.where("id").is(batchNumber);
		Criteria statusCriteria = Criteria.where("status").is(WithdrawRequest.STATUS_PROCESSING);
		Criteria andCriteria = new Criteria();
		andCriteria.andOperator(userCriteria, statusCriteria);
		query.addCriteria(andCriteria);
		WithdrawRequest request = mongoTemplate.findOne(query, WithdrawRequest.class);
		if (null == request) {
			logger.error("withdrawRequestCompleted - cannot locate WithdrawRequest object, invalid state");
			return;
		}
		Update update = new Update();
		update.set("status", WithdrawRequest.STATUS_PROCESSED);
		if (!mongoTemplate.updateFirst(query, update, WithdrawRequest.class).isUpdateOfExisting()) {
			logger.error("withdrawRequestCompleted - cannot update withdraw request object");
			return;
		}
		
		// update user account info
		Query userQuery = new Query();
		Criteria userIdCriteria = Criteria.where("phoneNumber").is(request.getUserId());
		userQuery.addCriteria(userIdCriteria);
		Update userUpdate = new Update();
		userUpdate.inc("balance", Integer.valueOf(request.getWithdrawAmount().intValue()*-1));
		userUpdate.inc("withdrawBalance", Integer.valueOf(request.getWithdrawAmount().intValue()*-1));
		userUpdate.set("withdrawStatus", User.WITHDRAW_STATUS_OPEN_FOR_WITHDRAW);
		if (!mongoTemplate.updateFirst(userQuery, userUpdate, User.class).isUpdateOfExisting()) {
			logger.error("withdrawRequestCompleted - cannot update user account info");
			return;
		}
	}
	
	public void withdrawRequestFailed(String batchNumber) {
		// locate withDrawRequest
		Query query = new Query();
		Criteria userCriteria = Criteria.where("id").is(batchNumber);
		Criteria statusCriteria = Criteria.where("status").is(WithdrawRequest.STATUS_PROCESSING);
		Criteria andCriteria = new Criteria();
		andCriteria.andOperator(userCriteria, statusCriteria);
		query.addCriteria(andCriteria);
		WithdrawRequest request = mongoTemplate.findOne(query, WithdrawRequest.class);
		if (null == request) {
			logger.error("withdrawRequestFailed - cannot locate WithdrawRequest object, invalid state");
			return;
		}
		Update update = new Update();
		update.set("status", WithdrawRequest.STATUS_PENDING);
		if (!mongoTemplate.updateFirst(query, update, WithdrawRequest.class).isUpdateOfExisting()) {
			logger.error("withdrawRequestFailed - cannot update withdraw request object");
			return;
		}
		
		// update user account info
		Query userQuery = new Query();
		Criteria userIdCriteria = Criteria.where("phoneNumber").is(request.getUserId());
		userQuery.addCriteria(userIdCriteria);
		Update userUpdate = new Update();
		userUpdate.set("withdrawStatus", User.WITHDRAW_STATUS_OPEN_FOR_WITHDRAW);
		if (!mongoTemplate.updateFirst(userQuery, userUpdate, User.class).isUpdateOfExisting()) {
			logger.error("withdrawRequestFailed - cannot update user account info");
			return;
		}		
	}
	
	public Map<String, String> updateIndividualInfo(UpdateIndividualInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate request parameters
		if ((null==req.getLegalName()) || (null==req.getProvince()) || (null==req.getCity()) || (null==req.getDistrict()) || (null==req.getAddress())) {
			if (logger.isDebugEnabled()) {
				logger.debug("updateIndividualInfo - missing required parameters");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
				
		// user can request verification only if previous request has processed, users are not allowed to request multiple pending requests
		if (!identityDataManager.canProceedIndividualVerificationPending(phoneNumber)) {
			if (logger.isWarnEnabled()) {
				logger.warn("updateIndividualInfo - multiple pending requests not allowed");
			}
			map.put("errorCode", "401");
			map.put("errorMessage", "multiple pending requests not allowed");
			return map;
		}
		
		// create pending
		IndividualPending pending = identityDataManager.findUpsertedIndividualPending(phoneNumber);
		if (pending == null) {
			pending = new IndividualPending();
		}
		pending.setPhoneNumber(phoneNumber);
		pending.setLegalName(req.getLegalName());
		pending.setProvince(req.getProvince());
		pending.setCity(req.getCity());
		pending.setDistrict(req.getDistrict());
		pending.setAddress(req.getAddress());
		if (null != req.getEmergencyContactName()) {
			pending.setEmergencyContactName(req.getEmergencyContactName());
		}
		if (null != req.getEmergencyContactPhoneNumber()) {
			pending.setEmergencyContactPhoneNumber(req.getEmergencyContactPhoneNumber());
		}
		if (!identityDataManager.upsertIndividualPending(pending, req.getRefId())) {
			if (logger.isErrorEnabled()) {
				logger.error("updateIndividualInfo - failed to upsert individual pending");
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to upsert request");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> updateIndividualCertInfo(UpdateIndividualCertInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// check parameters
		if ((null==req.getEmergencyContactName()) || (null==req.getEmergencyContactPhoneNumber())) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// update certification information
		if (!identityDataManager.updateIndividualCertInfo(phoneNumber, req)) {
			if (logger.isErrorEnabled()) {
				logger.error("updateIndividualCertInfo - failed to update certification information for " + phoneNumber);
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update information of certification");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> updateEnterpriseCertInfo(UpdateEnterpriseCertInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// check parameters
		if ((null==req.getEmergencyContactName()) || (null==req.getEmergencyContactPhoneNumber())) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// update certification information
		if (!identityDataManager.updateEnterpriseCertInfo(phoneNumber, req)) {
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update information of certification");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> updateEnterpriseInfo(UpdateEnterpriseInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate request parameters
		if ((null==req.getEnterpriseName()) || (null==req.getProvince()) || (null==req.getCity()) || (null==req.getDistrict()) || (null==req.getAddress()) || (null==req.getContactName()) || (null==req.getContactPhoneNumber())) {
			if (logger.isDebugEnabled()) {
				logger.debug("updateEnterpriseInfo - missing required parameters");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// user can request verification only if previous request has processed, users are not allowed to request multiple pending requests
		if (!identityDataManager.canProceedEnterpriseVerificationPending(phoneNumber)) {
			if (logger.isWarnEnabled()) {
				logger.warn("updateEnterpriseInfo - multiple pending requests not allowed");
			}
			map.put("errorCode", "401");
			map.put("errorMessage", "multiple pending requests not allowed");
			return map;
		}
		
		// create pending
		EnterprisePending pending = identityDataManager.findUpsertedEnterprisePending(phoneNumber);
		if (null == pending) {
			pending = new EnterprisePending();
		}
		pending.setPhoneNumber(phoneNumber);
		pending.setEnterpriseName(req.getEnterpriseName());
		pending.setProvince(req.getProvince());
		pending.setCity(req.getCity());
		pending.setDistrict(req.getDistrict());
		pending.setAddress(req.getAddress());
		pending.setContactName(req.getContactName());
		pending.setContactPhoneNumber(req.getContactPhoneNumber());
		if (!identityDataManager.upsertEnterprisePending(pending, req.getRefId())) {
			if (logger.isErrorEnabled()) {
				logger.error("updateEnterpriseInfo - failed to upsert individual pending");
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to upsert request");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> updateAvatar(String token, MultipartFile file) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate parameters
		if ((null==token) || (null==file)) {
			if (logger.isDebugEnabled()) {
				logger.debug("uploadCertificate - missing required parameters");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(token);
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		String avatarId = null;
		try {
			avatarId = identityDataManager.updateAvatar(phoneNumber, file);
			if (null == avatarId) {
				map.put("errorCode", "500");
				map.put("errorMessage", "failed to update");
				return map;
			}
		} catch (IOException e) {
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, token);
		
		map.put("avatarId", avatarId);
		return map;
	}
	
	public Map<String, String> cropAvatar(String token, String avatarId, String crop) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(token);
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate parameters
		if ((null==avatarId) || (null==crop)) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		if (crop.split(":").length != 3) {
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters - invalid crop");
			return map;
		}
		
		String userAvatarId = null;
		try {
			userAvatarId = identityDataManager.cropAvatar(phoneNumber, avatarId, crop);
			if (null == userAvatarId) {
				map.put("errorCode", "500");
				map.put("errorMessage", "failed to update");
				return map;
			}
		} catch (IOException e) {
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to update");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, token);
		
		map.put("avatarId", userAvatarId);
		return map;
	}
	
	public Map<String, String> uploadCertificate(String token, String type, MultipartFile file) {
		Map<String, String> map = new HashMap<String, String>();
		
		boolean isIndividual = true;
		
		// validate parameters
		if ((null==token) || (null==type) || (null==file)) {
			if (logger.isDebugEnabled()) {
				logger.debug("uploadCertificate - missing required parameters");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "missing parameters");
			return map;
		}
		if ((0!=type.compareTo("idfront")) && (0!=type.compareTo("idback")) && (0!=type.compareTo("license")) && (0!=type.compareTo("irecentphoto")) && 
				(0!=type.compareTo("erecentphoto")) && (0!=type.compareTo("eidfront")) && (0!=type.compareTo("eidback"))) {
			if (logger.isDebugEnabled()) {
				logger.debug("uploadCertificate - invalid type " + type);
			}
			map.put("errorCode", "402");
			map.put("errorMessage", "invalid parameters");
			return map;
		}
		if ((0==type.compareTo("license")) || (0==type.compareTo("erecentphoto")) || (0==type.compareTo("eidfront")) || (0==type.compareTo("eidback"))) {
			isIndividual = false;
		}
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(token);
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// user can request verification only if previous request has processed, users are not allowed to request multiple pending requests
		if (isIndividual) {
			if (!identityDataManager.canProceedIndividualVerificationPending(phoneNumber)) {
				if (logger.isWarnEnabled()) {
					logger.warn("updateIndividualInfo - multiple pending requests not allowed");
				}
				map.put("errorCode", "401");
				map.put("errorMessage", "multiple pending requests not allowed");
				return map;
			}
		} else {
			if (!identityDataManager.canProceedEnterpriseVerificationPending(phoneNumber)) {
				if (logger.isWarnEnabled()) {
					logger.warn("updateIndividualInfo - multiple pending requests not allowed");
				}
				map.put("errorCode", "401");
				map.put("errorMessage", "multiple pending requests not allowed");
				return map;
			}
		}
		
		// update certificate
		String imageId = null;
		if ((0==type.compareTo("license")) || (0==type.compareTo("erecentphoto")) || (0==type.compareTo("eidfront")) || (0==type.compareTo("eidback"))) {
			try {
				imageId = identityDataManager.updateEnterpriseCertificate(phoneNumber, type, file);
				if (null == imageId) {
					if (logger.isErrorEnabled()) {
						logger.error("uploadCertificate - failed to store ");
					}
					map.put("errorCode", "500");
					map.put("errorMessage", "failed to store");
					return map;
				}
			} catch (IOException e) {
				if (logger.isErrorEnabled()) {
					logger.error("uploadCertificate - failed to store " + e.getMessage());
				}
				map.put("errorCode", "500");
				map.put("errorMessage", "failed to store");
				return map;
			}
		} else {
			try {
				imageId = identityDataManager.updateIndividualCertificate(phoneNumber, type, file);
				if (null == imageId) {
					if (logger.isErrorEnabled()) {
						logger.error("uploadCertificate - failed to store ");
					}
					map.put("errorCode", "500");
					map.put("errorMessage", "failed to store");
					return map;
				}
			} catch (IOException e) {
				if (logger.isErrorEnabled()) {
					logger.error("uploadCertificate - failed to store " + e.getMessage());
				}
				map.put("errorCode", "500");
				map.put("errorMessage", "failed to store");
				return map;
			}
		}
		
		if (null != imageId) {
			map.put("imageId", imageId);
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, token);
		
		return map;
	}
	
	public void getCertificate(GetCertificateRequest req, HttpServletResponse res) throws IOException {
		// validate parameters
		if (null == req.getImageId()) {
			if (logger.isDebugEnabled()) {
				logger.debug("getCertificate - missing required parameters");
			}
			res.sendError(HttpServletResponse.SC_BAD_REQUEST, "missing parameters");
			return;
		}
		
		// validate token
		String adminPhoneNumber = (String)sessionManager.valueForAdminToken(req.getToken());
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if ((null==phoneNumber) && (null==adminPhoneNumber)) {
			if (logger.isDebugEnabled()) {
				logger.debug("getCertificate - token expired");
			}
			res.sendError(HttpServletResponse.SC_BAD_REQUEST, "token expired");
			return;
		}
		
		// send certificate back
		GridFSDBFile file = identityDataManager.getCertificate(req);
		if (null == file) {
			if (logger.isDebugEnabled()) {
				logger.debug("getCertificate - image not found, image id " + req.getImageId());
			}
			res.sendError(HttpServletResponse.SC_BAD_REQUEST, "image not found");
			return;
		}
		file.writeTo(res.getOutputStream());
		
		// reset token expiration
		if (null != phoneNumber) {
			sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		} else if (null != adminPhoneNumber) {
			sessionManager.resetAdminTokenExpiration(adminPhoneNumber, req.getToken());
		}
		
		return;
	}
	
	public Map<String, String> pendingIndividualInfo(PendingIndividualInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
				
		// compose result
		IndividualPending pending = identityDataManager.findUpsertedIndividualPending(phoneNumber);
		if (null == pending) {
			pending = identityDataManager.findInReviewIndividualPending(phoneNumber);
		}
		if (null == pending) {
			// no data
			return map;
		} else {
			if (null != pending.getLegalName()) {
				map.put("legalName", pending.getLegalName());
			}
			if (null != pending.getProvince()) {
				map.put("province", pending.getProvince());
			}
			if (null != pending.getCity()) {
				map.put("city", pending.getCity());
			}
			if (null != pending.getDistrict()) {
				map.put("district", pending.getDistrict());
			}
			if (null != pending.getAddress()) {
				map.put("address", pending.getAddress());
			}
			map.put("status", pending.getStatus());
			if (null != pending.getDescriptionOfReject()) {
				map.put("descriptionOfReject", pending.getDescriptionOfReject());
			}
			if (null != pending.getEmergencyContactName()) {
				map.put("emergencyContactName", pending.getEmergencyContactName());
			}
			if (null != pending.getEmergencyContactPhoneNumber()) {
				map.put("emergencyContactPhoneNumber", pending.getEmergencyContactPhoneNumber());
			}
			if (null != pending.getFrontImageId()) {
				map.put("frontImageId", pending.getFrontImageId());
			}
			if (null != pending.getBackImageId()) {
				map.put("backImageId", pending.getBackImageId());
			}
			if (null != pending.getRecentPhotoId()) {
				map.put("recentPhotoId", pending.getRecentPhotoId());
			}
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> pendingEnterpriseInfo(PendingEnterpriseInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// compose result
		EnterprisePending pending = identityDataManager.findUpsertedEnterprisePending(phoneNumber);
		if (null == pending) {
			pending = identityDataManager.findInReviewEnterprisePending(phoneNumber);
		}
		if (null == pending) {
			// no data
			return map;
		} else {
			if (null != pending.getEnterpriseName()) {
				map.put("enterpriseName", pending.getEnterpriseName());
			}
			if (null != pending.getProvince()) {
				map.put("province", pending.getProvince());
			}
			if (null != pending.getCity()) {
				map.put("city", pending.getCity());
			}
			if (null != pending.getDistrict()) {
				map.put("district", pending.getDistrict());
			}
			if (null != pending.getAddress()) {
				map.put("address", pending.getAddress());
			}
			if (null != pending.getContactName()) {
				map.put("contactName", pending.getContactName());
			}
			if (null != pending.getContactPhoneNumber()) {
				map.put("contactPhoneNumber", pending.getContactPhoneNumber());
			}
			map.put("status", pending.getStatus());
			if (null != pending.getDescriptionOfReject()) {
				map.put("descriptionOfReject", pending.getDescriptionOfReject());
			}
			if (null != pending.getLicenseImageId()) {
				map.put("licenseImageId", pending.getLicenseImageId());
			}
			if (null != pending.getFrontImageId()) {
				map.put("frontImageId", pending.getFrontImageId());
			}
			if (null != pending.getBackImageId()) {
				map.put("backImageId", pending.getBackImageId());
			}
			if (null != pending.getRecentPhotoId()) {
				map.put("recentPhotoId", pending.getRecentPhotoId());
			}
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> completeIndividualInfo(CompleteIndividualInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate IndividualPending
		IndividualPending pending = identityDataManager.findUpsertedIndividualPending(phoneNumber);
		if (null == pending) {
			if (logger.isDebugEnabled()) {
				logger.debug("completeIndividualInfo - invalid state, no upserted individual info");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "invalid state");
			return map;
		}
		if ((null==pending.getFrontImageId()) || (null==pending.getBackImageId()) || (null==pending.getRecentPhotoId())) {
			if (logger.isDebugEnabled()) {
				logger.debug("completeIndividualInfo - invalid state, missing id card front or back");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "invalid state");
			return map;
		}
		
		// mark completed
		if (!identityDataManager.completeIndividualPending(pending)) {
			if (logger.isErrorEnabled()) {
				logger.error("completeIndividualInfo - failed to update individual info, phone number: " + phoneNumber);
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to complete");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> completeEnterpriseInfo(CompleteEnterpriseInfoRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// validate EnterprisePending
		EnterprisePending pending = identityDataManager.findUpsertedEnterprisePending(phoneNumber);
		if ((null==pending) || (null==pending.getLicenseImageId()) || (null==pending.getFrontImageId()) || (null==pending.getBackImageId()) || (null==pending.getRecentPhotoId())) {
			if (logger.isDebugEnabled()) {
				logger.debug("completeEnterpriseInfo - invalid state, no upserted individual info");
			}
			map.put("errorCode", "400");
			map.put("errorMessage", "invalid state");
			return map;
		}
		
		// mark completed
		if (!identityDataManager.completeEnterprisePending(pending)) {
			if (logger.isErrorEnabled()) {
				logger.error("completeEnterpriseInfo - failed to update individual info, phone number: " + phoneNumber);
			}
			map.put("errorCode", "500");
			map.put("errorMessage", "failed to complete");
			return map;
		}
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return map;
	}
	
	public Map<String, String> accountBalance(AccountBalanceRequest req) {
		Map<String, String> map = new HashMap<String, String>();
		
		// validate token
		String phoneNumber = (String)sessionManager.valueForRestToken(req.getToken());
		if (null == phoneNumber) {
			map.put("errorCode", "300");
			map.put("errorMessage", "token expired");
			return map;
		}
		
		// retrieve balance
		Map<String, String> result = identityDataManager.accountBalance(phoneNumber);
		
		// reset token expiration
		sessionManager.resetRestTokenExpiration(phoneNumber, req.getToken());
		
		return result;
	}

	@Autowired
	private IdentityDataManager identityDataManager;
	
	@Autowired
	private SessionManager sessionManager;
	
	@Autowired
	private MongoTemplate mongoTemplate;
	
	@Autowired
	private MessageController messageController;
	
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;
	
	private static final Logger logger = LoggerFactory.getLogger(IdentityController.class);
	
	private static final String FREQUENCY_KEY_SUFFIX = "_FREQ_KEY";
	private static final String VERIFICATION_KEY_SUFFIX = "_VER_KEY";
	
}
